# mp3_mp4.py：卡点视频
# 视频要先处理一下 找到打击点 加强
#
# 1. 获取音频振幅图
# 2. 获取振幅包络
# 3. 获取mp4音频大于某个值的位置点  包络拐点不合适，可能是从小声音到没声音
# 4. 计算两组数据的拟合曲线，固定mp3时间轴不变，利用mp4以秒为遍历单位进行5s窗口的函数拟合 5s内的点数 滑动比较mp4不同位置的准确度
# 5. 找到最好的拟合曲线的函数
# 6，包络相似即卡点后进行合并，音频对应的视频空白位置用黑屏
# 7. 合并后mp3静音，mp4对应的拐点位置声音放大 -- 这一步手动操作 脚本侧重点是合到一起
# 8. 传入的接口是音乐名字和对应的视频文件夹，视频不需要截取，自动进行完整版匹配，匹配好后进行自动剪切

import os

import matplotlib.pyplot as plt
import numpy as np
import librosa
import librosa.display
import IPython.display as ipd
from scipy import stats
import ffmpeg
import moviepy.editor as mp

SIMPLE_POINT_TIME = 5  # 音频采样及对比时间窗口是5秒 每5秒采一次
MP4_FAST_SLOW = 2  # 视频快进和慢放的比例是2倍

FRAME_SIZE = 1024  # 也叫window size， 指划分信号的固定间隔的长度
HOP_LENGTH = 512  # 一个窗口中不关心部分的长度，win_length/hop_length 作为样本量

class sound_pro:   # 声音相关操作
    def __init__(self, mp3_path):  # 不同电脑的gui坐标值不一样
        self.mp3_path = mp3_path
        self.mp3_time = 0
        self.mp3_bpm = 0

    def amplitude_envelope(signal, frame_size, hop_length):  # 进行音频振幅分片计算
        amplitude_envelope = []
        # calculate amplitude envelope for each frame
        for i in range(0, len(signal), hop_length):
            amplitude_envelope_current_frame = max(signal[i:i + frame_size])
            amplitude_envelope.append(amplitude_envelope_current_frame)
        return np.array(amplitude_envelope)

    def get_mp3_point(self):  # 获取音频包络中拐点的位置  单位秒
        # y ,sr= librosa.load(music_path, offset=start_time, duration=duration,sr=sr)  # duration截取时长
        if str(self.mp3_path).find(".mp3") > 0:
            y, sr = librosa.load(self.mp3_path)  # 读取音频， 返回数字信号以及采样频率
        elif self.mp3_path.find(".mp4") > 0:   # 先将mp4转换为mp4
            mp4_mp3_path = self.mp3_path[:-4] + ".mp3"
            # print(os.path.exists(mp4_mp3_path), mp4_mp3_path)
            if os.path.exists(mp4_mp3_path) == False:  # 不存在才剪切
                my_clip = mp.VideoFileClip(self.mp3_path)
                my_clip.audio.write_audiofile(mp4_mp3_path)
                print(mp4_mp3_path)
            y, sr = librosa.load(mp4_mp3_path)  # 读取音频， 返回数字信号以及采样频率

        self.mp3_time = 1 / sr * len(y)  # 可以让我们计算音频的时长。

        tempo, beats = librosa.beat.beat_track(y=y, sr=sr)
        self.mp3_bpm = int(tempo)
        # beat_times = list(librosa.frames_to_time(beats, sr=sr))
        # print(self.mp3_bpm, beat_times)

        # 计算振幅包络并可视化
        ae_debussy = sound_pro.amplitude_envelope(y, FRAME_SIZE, HOP_LENGTH)

        frames = range(len(ae_debussy))
        t = librosa.frames_to_time(frames, hop_length=HOP_LENGTH)
        rms_debussy = librosa.feature.rms(y, frame_length=FRAME_SIZE, hop_length=HOP_LENGTH)[0]
        # print(t, rms_debussy)

        px = video_pro.vis(rms_debussy)
        # 用时间点显示
        # plt.figure(figsize=(10, 5))
        # plt.scatter(t[px], y[px], color="r")  # px是x轴第几个值
        # plt.show()

        sound_point = np.round(t[px], 2).tolist()
        print(sound_point)
        return sound_point

    def get_mp3_bpm(self):  # 获取音频节拍
        print(self.mp3_path)
        return self.mp3_bpm



class video_pro:   # 视频相关操作
    def line(mp3_point, mp4_point):  # 求音视频数据的线性回归参数 mp3_point mp3高音点  mp4_point  mp4高音点  默认mp3_point >= mp4_point
      import numpy as np
      from numpy.linalg import solve
      sumx = np.sum(mp3_point)  # x列的和
      sumy = np.sum(mp4_point)  # y列的和
      n = len(mp3_point)  # 数据的个数
      sumxy = np.sum(np.array(mp3_point)*np.array(mp4_point))  # xy两列的乘积的和
      sumx2 = np.sum(np.array(mp3_point)**2)  # x的平方的和
      a = np.mat([[sumx, n], [sumx2, sumx]])  # 用np.mat传入第一个矩阵
      b = np.mat([sumy, sumxy]).T  # 用np.mat.T传入第二个矩阵
      x = solve(a, b)  # 求出ab
      return x  # 输出  第一个是视频快进或缩小比例 第二个是前后移动时间

    def closest(mylist, Number):  # 从整数列表（数组）中获取最接近给定值的数字
        answer = []
        for i in mylist:
            answer.append(abs(Number - i))
        return answer.index(min(answer))


    def AMPD(data):
        """
        实现AMPD算法
        :param data: 1-D numpy.ndarray
        :return: 波峰所在索引值的列表
        """
        p_data = np.zeros_like(data, dtype=np.int32)
        count = data.shape[0]
        arr_rowsum = []
        for k in range(1, count // 2 + 1):
            row_sum = 0
            for i in range(k, count - k):
                if data[i] > data[i - k] and data[i] > data[i + k]:
                    row_sum -= 1
            arr_rowsum.append(row_sum)
        min_index = np.argmin(arr_rowsum)
        max_window_length = min_index
        for k in range(1, max_window_length + 1):
            for i in range(k, count - k):
                if data[i] > data[i - k] and data[i] > data[i + k]:
                    p_data[i] += 1
        return np.where(p_data == max_window_length)[0]

    def vis(sim_data):
        y = sim_data
        plt.plot(range(len(y)), y)
        px = video_pro.AMPD(y)

        middle_value = (max(y[px]) + min(y[px]) ) / 2  # 低音时的声音拐点不考虑
        if middle_value > (max(y[px]) * 2 / 3):
            middle_value = (max(y[px]) * 2 / 3)

        for v in px:
            if y[v] < middle_value:
                # print(v, y[v], middle_value)
                indices = np.where(px == v)
                px = np.delete(px, indices)

        # 用坐标点显示
        # plt.scatter(px, y[px], color="red")  # px是x轴第几个值
        # plt.show()

        # print(time[px])
        # print(px)

        return px  # 返回拐点横坐标  矩阵格式

    def mp4_creat(mp4_path, mp4_start, mp4_end, mp4_radio, mp3_start):  # mp4_path 要剪切的素材文件 后面参数是mp4的开始结束时间，这段时间要放到的位置  mp4_radio是视频快慢放的速率
        print(mp4_path)


    def cir_compare(mp3_point, mp4_point):  # 循环比较  固定mp3时间轴 进行滑动比较  SIMPLE_POINT_TIME  返回字典分别是结果，mp3_point,mp4_point
        result_dict = {}
        result_dict["result"] = 0

        # print(mp3_point[0] + SIMPLE_POINT_TIME)
        mp3_loc_now = mp3_point[0]  # 当前点位置
        mp3_loc_next = mp3_point[1]  # 下一点位置
        mp4_loc_now = mp4_point[0]  # 当前点位置
        mp4_loc_next = mp4_point[1]  # 下一点位置
        mp3_idx = 1
        radio_list = []
        while mp3_idx < len(mp3_point):  # 时间范围内就一直循环
            if mp3_loc_next - mp3_loc_now > SIMPLE_POINT_TIME:  # 如果就一个点认为是特殊情况 然后从下一个点开始
                # 在mp4_point中找一个同样是此特殊情况的  找到特殊情况将其置0
                # for mp4_loc_now in mp4_point[:-1]:
                #     mp4_loc_next = mp4_point[ mp4_point.index(mp4_loc_now) + 1 ]
                #     if mp4_loc_now != 0 and mp4_loc_next != 0 and mp3_loc_now != 0 and mp3_loc_next != 0:  # 没有为0的地方
                #         if mp4_loc_next - mp4_loc_now > SIMPLE_POINT_TIME: # 也是特殊情况
                #             mp4_point[mp4_point.index(mp4_loc_now)] = 0  # 标记此点已被使用
                #             video_pro.mp4_creat(mp4_path, mp4_loc_now, mp4_loc_now+SIMPLE_POINT_TIME, 1, mp3_loc_now)  # 进行混剪视频输出
                #             result_dict["result"] = 1
                #             break  # 不再循环查找

                mp3_loc_now = mp3_loc_next
                mp3_point[mp3_idx-1] = 0
                mp3_idx = mp3_idx + 1
                mp3_loc_next = mp3_point[mp3_idx]

                # print("单个点是", mp3_loc_now)

            else:  # 多个点进行比例计算
                cur_mp3_point_list = []
                cur_mp4_point_list = []
                mp3_loc_now_temp = mp3_loc_now
                while mp3_loc_now <= (mp3_loc_now_temp + SIMPLE_POINT_TIME) and mp3_idx < len(mp3_point):  # 获取窗口时间内的点数
                    cur_mp3_point_list.append(mp3_loc_now)

                    mp3_loc_now = mp3_loc_next
                    mp3_idx = mp3_idx + 1
                    if mp3_idx == len(mp3_point):
                        cur_mp3_point_list.append(mp3_loc_now)
                        break
                    mp3_loc_next = mp3_point[mp3_idx]

                window_point_num = len(cur_mp3_point_list)  # 有几个点
                # print("5s内的点数和内容", window_point_num, cur_mp3_point_list, "\r\n")

                # 在mp4_point中以window_point_num为窗口滑动进行计算k值  计算完毕将mp4这组point置0
                for mp4_loc_now in mp4_point[:-1]:
                    mp4_inx = mp4_point.index(mp4_loc_now)
                    mp4_inx_win = mp4_inx + window_point_num
                    if mp4_inx_win > len(mp4_point):  # 超出索引不进行操作
                        break
                    cur_mp4_point_list = mp4_point[mp4_inx:mp4_inx_win]
                    # print(cur_mp4_point_list)
                    if (0 in cur_mp4_point_list) or (0 in cur_mp3_point_list):  # 有一个0认为中间有已经匹配的值 不进行后续计算
                        break

                    radio = video_pro.line(cur_mp4_point_list, cur_mp3_point_list)
                    radio_dict = {}
                    radio_dict["mp4"] = cur_mp4_point_list
                    radio_dict["mp3"] = cur_mp3_point_list
                    radio_dict["kp"] = (radio.tolist()[0] + radio.tolist()[1])
                    radio_list.append(radio_dict)

        print(radio_list)
        # 在系数里面找一个最合适的进行剪切，剪切后将mp4对应点位置置0 返回1 如果找不到（超出范围）返回0 不再进行查找
        k_list = []
        p_list = []
        for dict in radio_list:  # 列表中是字典
            k_list.append(dict.get("kp")[0])
            p_list.append(dict.get("kp")[1])

        best_loc = video_pro.closest(k_list, 1)  # 曲线最接近的值
        best_k = k_list[best_loc]
        best_list = radio_list[best_loc]
        print(best_k, best_loc, best_list)

        if best_k > 1*MP4_FAST_SLOW or best_k < 1/MP4_FAST_SLOW:  # 超过快放慢放的速度
            result_dict["mp3"] = mp3_point
            result_dict["mp4"] = mp4_point
            result_dict["result"] = 0
            return result_dict



        result_dict["mp3"] = mp3_point
        result_dict["mp4"] = mp4_point

        window_point_num = len(best_list["mp4"])  # 用0标记window_point_num内点已被使用
        mp3_mark_loc = mp3_point.index(best_list["mp3"][0])  # 获取mp3标记点的位置
        mp4_mark_loc = mp4_point.index(best_list["mp4"][0])
        print(window_point_num, mp3_mark_loc, mp4_mark_loc)
        for v in range(0, window_point_num):
            mp3_point[mp3_mark_loc + v] = 0
            mp4_point[mp4_mark_loc + v] = 0

        video_pro.mp4_creat(mp4_path, mp4_loc_now, mp4_loc_now+SIMPLE_POINT_TIME, 1, mp3_loc_now)  # 进行混剪视频输出
        result_dict["result"] = 1

        # print(result_dict)
        return result_dict


    def window_check(mp4_path):  # 进行串口时间内的最佳拟合曲线检查
        sound_mp4 = sound_pro(mp4_path)

        mp3_point = sound_mp3.get_mp3_point()
        mp4_point = sound_mp4.get_mp3_point()

        result_dict = video_pro.cir_compare(mp3_point, mp4_point)
        result = result_dict["result"]
        mp3_point = result_dict["mp3"]
        mp4_point = result_dict["mp4"]
        # while result == 1:
        #     result_dict = video_pro.cir_compare(mp3_point, mp4_point)
        #     result = result_dict["result"]
        #     mp3_point = result_dict["mp3"]
        #     mp4_point = result_dict["mp4"]

        print(mp4_path, "视频文件卡点结束, mp3和mp4的剪切颗粒度：\r\n")
        print(mp3_point, "\r\n")
        print(mp4_point, "\r\n")
























if __name__ == '__main__':
    sound_mp3 = sound_pro("D:\\video\\music_temp\\混剪\\Tomoyasu Hotei - Battle Without Honor Or Humanity.mp3")
    video_pro.window_check("D:\\video\\video_temp\\刺客伍六七\\粗剪\\卡点.mp4")

    # mp3_point = [1, 2, 3, 4, 5]
    # mp4_point = [1, 2, 2.5, 3.5, 5]
    # print(video_pro.line(mp4_point, mp3_point))